Skip to main content
Glama

Edit MCP

documents.rs10.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::collections::LinkedList; use std::ffi::OsStr; use std::fs::File; use std::path::{Path, PathBuf}; use edit::buffer::{RcTextBuffer, TextBuffer}; use edit::helpers::{CoordType, Point}; use edit::{apperr, path, sys}; use crate::state::DisplayablePathBuf; pub struct Document { pub buffer: RcTextBuffer, pub path: Option<PathBuf>, pub dir: Option<DisplayablePathBuf>, pub filename: String, pub file_id: Option<sys::FileId>, pub new_file_counter: usize, } impl Document { pub fn save(&mut self, new_path: Option<PathBuf>) -> apperr::Result<()> { let path = new_path.as_deref().unwrap_or_else(|| self.path.as_ref().unwrap().as_path()); let mut file = DocumentManager::open_for_writing(path)?; { let mut tb = self.buffer.borrow_mut(); tb.write_file(&mut file)?; } if let Ok(id) = sys::file_id(None, path) { self.file_id = Some(id); } if let Some(path) = new_path { self.set_path(path); } Ok(()) } pub fn reread(&mut self, encoding: Option<&'static str>) -> apperr::Result<()> { let path = self.path.as_ref().unwrap().as_path(); let mut file = DocumentManager::open_for_reading(path)?; { let mut tb = self.buffer.borrow_mut(); tb.read_file(&mut file, encoding)?; } if let Ok(id) = sys::file_id(None, path) { self.file_id = Some(id); } Ok(()) } fn set_path(&mut self, path: PathBuf) { let filename = path.file_name().unwrap_or_default().to_string_lossy().into_owned(); let dir = path.parent().map(ToOwned::to_owned).unwrap_or_default(); self.filename = filename; self.dir = Some(DisplayablePathBuf::from_path(dir)); self.path = Some(path); self.update_file_mode(); } fn update_file_mode(&mut self) { let mut tb = self.buffer.borrow_mut(); tb.set_ruler(if self.filename == "COMMIT_EDITMSG" { 72 } else { 0 }); } } #[derive(Default)] pub struct DocumentManager { list: LinkedList<Document>, } impl DocumentManager { #[inline] pub fn len(&self) -> usize { self.list.len() } #[inline] pub fn active(&self) -> Option<&Document> { self.list.front() } #[inline] pub fn active_mut(&mut self) -> Option<&mut Document> { self.list.front_mut() } #[inline] pub fn update_active<F: FnMut(&Document) -> bool>(&mut self, mut func: F) -> bool { let mut cursor = self.list.cursor_front_mut(); while let Some(doc) = cursor.current() { if func(doc) { let list = cursor.remove_current_as_list().unwrap(); self.list.cursor_front_mut().splice_before(list); return true; } cursor.move_next(); } false } pub fn remove_active(&mut self) { self.list.pop_front(); } pub fn add_untitled(&mut self) -> apperr::Result<&mut Document> { let buffer = Self::create_buffer()?; let mut doc = Document { buffer, path: None, dir: Default::default(), filename: Default::default(), file_id: None, new_file_counter: 0, }; self.gen_untitled_name(&mut doc); self.list.push_front(doc); Ok(self.list.front_mut().unwrap()) } pub fn gen_untitled_name(&self, doc: &mut Document) { let mut new_file_counter = 0; for doc in &self.list { new_file_counter = new_file_counter.max(doc.new_file_counter); } new_file_counter += 1; doc.filename = format!("Untitled-{new_file_counter}.txt"); doc.new_file_counter = new_file_counter; } pub fn add_file_path(&mut self, path: &Path) -> apperr::Result<&mut Document> { let (path, goto) = Self::parse_filename_goto(path); let path = path::normalize(path); let mut file = match Self::open_for_reading(&path) { Ok(file) => Some(file), Err(err) if sys::apperr_is_not_found(err) => None, Err(err) => return Err(err), }; let file_id = if file.is_some() { Some(sys::file_id(file.as_ref(), &path)?) } else { None }; // Check if the file is already open. if file_id.is_some() && self.update_active(|doc| doc.file_id == file_id) { let doc = self.active_mut().unwrap(); if let Some(goto) = goto { doc.buffer.borrow_mut().cursor_move_to_logical(goto); } return Ok(doc); } let buffer = Self::create_buffer()?; { if let Some(file) = &mut file { let mut tb = buffer.borrow_mut(); tb.read_file(file, None)?; if let Some(goto) = goto && goto != Default::default() { tb.cursor_move_to_logical(goto); } } } let mut doc = Document { buffer, path: None, dir: None, filename: Default::default(), file_id, new_file_counter: 0, }; doc.set_path(path); if let Some(active) = self.active() && active.path.is_none() && active.file_id.is_none() && !active.buffer.borrow().is_dirty() { // If the current document is a pristine Untitled document with no // name and no ID, replace it with the new document. self.remove_active(); } self.list.push_front(doc); Ok(self.list.front_mut().unwrap()) } pub fn reflow_all(&self) { for doc in &self.list { let mut tb = doc.buffer.borrow_mut(); tb.reflow(); } } pub fn open_for_reading(path: &Path) -> apperr::Result<File> { File::open(path).map_err(apperr::Error::from) } pub fn open_for_writing(path: &Path) -> apperr::Result<File> { File::create(path).map_err(apperr::Error::from) } fn create_buffer() -> apperr::Result<RcTextBuffer> { let buffer = TextBuffer::new_rc(false)?; { let mut tb = buffer.borrow_mut(); tb.set_insert_final_newline(!cfg!(windows)); // As mandated by POSIX. tb.set_margin_enabled(true); tb.set_line_highlight_enabled(true); } Ok(buffer) } // Parse a filename in the form of "filename:line:char". // Returns the position of the first colon and the line/char coordinates. fn parse_filename_goto(path: &Path) -> (&Path, Option<Point>) { fn parse(s: &[u8]) -> Option<CoordType> { if s.is_empty() { return None; } let mut num: CoordType = 0; for &b in s { if !b.is_ascii_digit() { return None; } let digit = (b - b'0') as CoordType; num = num.checked_mul(10)?.checked_add(digit)?; } Some(num) } fn find_colon_rev(bytes: &[u8], offset: usize) -> Option<usize> { (0..offset.min(bytes.len())).rev().find(|&i| bytes[i] == b':') } let bytes = path.as_os_str().as_encoded_bytes(); let colend = match find_colon_rev(bytes, bytes.len()) { // Reject filenames that would result in an empty filename after stripping off the :line:char suffix. // For instance, a filename like ":123:456" will not be processed by this function. Some(colend) if colend > 0 => colend, _ => return (path, None), }; let last = match parse(&bytes[colend + 1..]) { Some(last) => last, None => return (path, None), }; let last = (last - 1).max(0); let mut len = colend; let mut goto = Point { x: 0, y: last }; if let Some(colbeg) = find_colon_rev(bytes, colend) { // Same here: Don't allow empty filenames. if colbeg != 0 && let Some(first) = parse(&bytes[colbeg + 1..colend]) { let first = (first - 1).max(0); len = colbeg; goto = Point { x: last, y: first }; } } // Strip off the :line:char suffix. let path = &bytes[..len]; let path = unsafe { OsStr::from_encoded_bytes_unchecked(path) }; let path = Path::new(path); (path, Some(goto)) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_last_numbers() { fn parse(s: &str) -> (&str, Option<Point>) { let (p, g) = DocumentManager::parse_filename_goto(Path::new(s)); (p.to_str().unwrap(), g) } assert_eq!(parse("123"), ("123", None)); assert_eq!(parse("abc"), ("abc", None)); assert_eq!(parse(":123"), (":123", None)); assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 0, y: 122 }))); assert_eq!(parse("45:123"), ("45", Some(Point { x: 0, y: 122 }))); assert_eq!(parse(":45:123"), (":45", Some(Point { x: 0, y: 122 }))); assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 122, y: 44 }))); assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 0, y: 122 }))); assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 2, y: 1 }))); assert_eq!(parse("::3"), (":", Some(Point { x: 0, y: 2 }))); assert_eq!(parse("1::3"), ("1:", Some(Point { x: 0, y: 2 }))); assert_eq!(parse(""), ("", None)); assert_eq!(parse(":"), (":", None)); assert_eq!(parse("::"), ("::", None)); assert_eq!(parse("a:1"), ("a", Some(Point { x: 0, y: 0 }))); assert_eq!(parse("1:a"), ("1:a", None)); assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 0, y: 9 }))); assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 4, y: 9 }))); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/edit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server